package top.lshaci.learning.oauth2.server.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.authserver.AuthorizationServerProperties;
import org.springframework.boot.autoconfigure.security.oauth2.authserver.AuthorizationServerTokenServicesConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import top.lshaci.learning.oauth2.server.model.User;

import javax.sql.DataSource;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.*;

/**
 * Oauth2ServerConfig
 *
 * <pre>
 *     1、该类进行配置时，会覆盖自动配置类 OAuth2AuthorizationServerConfiguration
 *     导致：
 *         1）AuthorizationServerProperties 配置文件不会加载
 *         2）JwtKeyStoreConfiguration 也不会自动配置
 *     2、开启 AuthorizationServerProperties 配置，用于 AuthorizationServerTokenServicesConfiguration 配置类
 *         1）{@code @EnableConfigurationProperties(AuthorizationServerProperties.class)}
 *         2）{@code @Import(AuthorizationServerTokenServicesConfiguration.class)}
 *         引入该配置，主要是为了得到 JwtKeyStoreConfiguration 中配置的 TokenStore 和 JwtAccessTokenConverter 两个 bean
 *         从而不需要自己去配置这两个 bean
 * </pre>
 *
 * @author lshaci
 * @since 1.0.0
 */
@Slf4j
@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(AuthorizationServerProperties.class)
@Import(AuthorizationServerTokenServicesConfiguration.class)
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter implements ApplicationContextAware {

    private ApplicationContext context;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private AuthorizationServerProperties authorizationServerProperties;

    /**
     * 配置客户端详情
     * <pre>
     *     客户端信息保存到数据
     *     使用 JdbcClientDetailsService
     * </pre>
     *
     * @param clients 客户端详情培训
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenStore(tokenStore);
        // 访问令牌的默认有效性（以秒为单位）默认12小时
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
        // 刷新令牌的有效性（以秒为单位）默认30天
        tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancerList = new ArrayList<>();
        enhancerList.add(jwtTokenEnhancer());
        enhancerList.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(enhancerList);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        // 支持 password 模式
        endpoints.authenticationManager(authenticationManager);
        // 授权码服务 (保存到数据库)
        endpoints.authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource));
        // 令牌管理服务
        endpoints.tokenServices(tokenServices);
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许客户端访问 OAuth2 授权接口，否则请求 token 会返回 401
        security.allowFormAuthenticationForClients();
        // 地址 /oauth/token_key 是公开 【提供公有密匙的端点】
        security.tokenKeyAccess("permitAll()");
        // 地址 /oauth/check_token 公开
        security.checkTokenAccess("permitAll()");
    }

    @Bean
    public TokenEnhancer jwtTokenEnhancer() {
        log.debug("Config jwtTokenEnhancer.");
        return (accessToken, authentication) -> {
            Map<String, Object> map = new HashMap<>(8);
            User user = (User) authentication.getUserAuthentication().getPrincipal();
            map.put("id", user.getId());
            map.put("client_id", user.getClientId());
            map.put("user_name", user.getUsername());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            return accessToken;
        };
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }

    /**
     * spring cloud gateway 需要获取 JWKSet 类型的公钥
     *
     * @return JWKSet
     */
    @Bean
    public JWKSet jwkSet() {
        log.debug("Config JWKSet.");
        AuthorizationServerProperties.Jwt jwt = authorizationServerProperties.getJwt();
        Resource keyStore = this.context.getResource(jwt.getKeyStore());
        char[] keyStorePassword = jwt.getKeyStorePassword().toCharArray();
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keyStore, keyStorePassword);

        String keyAlias = jwt.getKeyAlias();
        char[] keyPassword = Optional.ofNullable(jwt.getKeyPassword())
                .map(String::toCharArray).orElse(keyStorePassword);
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword);
        RSAKey key = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()).build();
        return new JWKSet(key);
    }

}
